﻿// -----------------------------------------------------------------------
// <copyright file="ServerSocket.cs" company="FastNET Services">
//     Copyright (c) 2008-2009. All Rights Reserved.
// </copyright>
// <license>
//     GPLv2
// </license>
// <warranty>
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
//     BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//     DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </warranty>
// -----------------------------------------------------------------------
#region Usings

using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
using GateShift.Common.Networking.Enumerations;
using GateShift.Common.Networking.Events;
using GateShift.Common.Networking.Structures;

#endregion

namespace GateShift.Servers.GameServer.Networking
{
    /// <summary>
    /// Standard server networking class
    /// </summary>
    internal class ServerSocket
    {
        // See Codes: http://msdn.microsoft.com/en-us/library/ms740668.aspx
        #region Variables
        private readonly object _syncObject;
        private readonly Socket _socket;
        private bool _firedDisconnection;
        #endregion

        #region CTor/DTor

        /// <summary>
        /// Initializes a new instance of the ServerSocket class
        /// </summary>
        /// <param name="socket">Socket to manage for this instance</param>
        public ServerSocket(Socket socket)
        {
            if (socket == null)
            {
                throw new ArgumentNullException("socket");
            }

            _socket = socket;

            ConnectionID = Guid.NewGuid();
            _syncObject = new object();

            // The socket will linger for 10 seconds after 
            // Socket.Close is called.
            _socket.LingerState = new LingerOption(true, 10);

            // Disable the Nagle Algorithm for this tcp socket.
            _socket.NoDelay = true;

            // Set the receive buffer size to 8k
            _socket.ReceiveBufferSize = 8192;

            // Set the send buffer size to 8k.
            _socket.SendBufferSize = 8192;

            // Set the Time To Live (TTL) to 42 router hops.
            _socket.Ttl = 42;

            // Start recieving immediately
            BeginReceiveHeader();
        }

        #endregion

        #region Public Events

        /// <summary>
        /// Event raised when the connection to the server has been terminated
        /// </summary>
        public event EventHandler Disconnected;

        /// <summary>
        /// Event raised when data is about to be sent to the server
        /// </summary>
        public event EventHandler<SendBeginEventArgs> SendBegin;

        /// <summary>
        /// Event raised when data has been sucessfully sent to the server
        /// </summary>
        public event EventHandler<SendCompleteEventArgs> SendComplete;

        /// <summary>
        /// Event raised when data is about to be recieved from the server
        /// </summary>
        public event EventHandler<RecieveBeginEventArgs> RecieveBegin;

        /// <summary>
        /// Event raised when data has been recieved from the server
        /// </summary>
        public event EventHandler<RecieveCompleteEventArgs> RecieveComplete;

        /// <summary>
        /// Event raised when a complete set of data has been recieved from the server and is ready to be handled
        /// </summary>
        public event EventHandler<PacketRecievedEventArgs> PacketRecieved;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the current connection ID for this instance
        /// </summary>
        public Guid ConnectionID { get; private set; }

        /// <summary>
        /// Gets a value indicating whether or not there is an active connection
        /// </summary>
        public bool IsConnected
        {
            get
            {
                try
                {
                    return _socket.Connected;
                }
                catch
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Gets the remote IP and Port the connection is bound to. Returns NULL if there is no connection.
        /// </summary>
        public IPEndPoint RemoteEndPoint
        {
            get
            {
                try
                {
                    return _socket.RemoteEndPoint as IPEndPoint;
                }
                catch
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// Gets the local IP and Port the connection is bound to. Returns NULL if there is no connection.
        /// </summary>
        public IPEndPoint LocalEndPoint
        {
            get
            {
                try
                {
                    return _socket.LocalEndPoint as IPEndPoint;
                }
                catch
                {
                    return null;
                }
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Disconnect client from the server
        /// </summary>
        public void Disconnect()
        {
            // If we are not connected there is no need to continue
            if (!IsConnected)
            {
                return;
            }

            // Try to disconnect
            try
            {
                _socket.Shutdown(SocketShutdown.Both);
                _socket.BeginDisconnect(false, EndDisconnect, _socket);
            }
            catch (SocketException)
            {
                return;
            }
        }

        /// <summary>
        /// Starts sending a DataPacket to the server
        /// </summary>
        /// <param name="packet">DataPacket to send</param>
        public void SendPacket(DataPacket packet)
        {
            // If we are not connected, do not continue
            if (!IsConnected)
            {
                return;
            }

            // Use a memory stream so we do not have to resize arrays
            MemoryStream outStream = new MemoryStream();

            // Write Flags
            outStream.WriteByte((byte)packet.Flags);

            // Write Codes
            outStream.WriteByte(packet.PrimaryCode);
            outStream.WriteByte(packet.SecondaryCode);

            // Write the size of the data
            outStream.Write(BitConverter.GetBytes(packet.DataSize), 0, 4);

            // Write the data, if any exists
            if (packet.Data != null)
            {
                outStream.Write(packet.Data, 0, packet.Data.Length);
            }

            // Notify we are starting the send
            OnSendBegin(new SendBeginEventArgs((int)outStream.Length));

            // Now make the call to send the data
            try
            {
                _socket.BeginSend(outStream.ToArray(), 0, (int)outStream.Length, SocketFlags.None, new AsyncCallback(EndSendPacket), packet);
            }
            catch (SocketException e)
            {
                // Handle specific errors
                switch (e.SocketErrorCode)
                {
                    case SocketError.ConnectionReset: // Forced Disconnect
                        FinalizeDisconnection();
                        break;
                    default: // Everything else, rethrow
                        throw;
                }
            }
        }

        #endregion

        #region Event Methods

        /// <summary>
        /// Calls the Disconnected event in a thread safe manner
        /// </summary>
        protected void OnDisconnected()
        {
            if (Disconnected == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = Disconnected.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(Disconnected, new object[] { this, EventArgs.Empty });
            }
            else
            {
                Disconnected.Invoke(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Calls the SendBegin event in a thread safe manner
        /// </summary>
        /// <param name="e">SendBeginEventArgs with information about the event</param>
        protected void OnSendBegin(SendBeginEventArgs e)
        {
            if (SendBegin == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = SendBegin.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(SendBegin, new object[] { this, e });
            }
            else
            {
                SendBegin.Invoke(this, e);
            }
        }

        /// <summary>
        /// Calls the SendComplete event in a thread safe manner
        /// </summary>
        /// <param name="e">SendCompleteEventArgs with information about the event</param>
        protected void OnSendComplete(SendCompleteEventArgs e)
        {
            if (SendComplete == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = SendComplete.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(SendComplete, new object[] { this, e });
            }
            else
            {
                SendComplete.Invoke(this, e);
            }
        }

        /// <summary>
        /// Calls the RecieveBegin event in a thread safe manner
        /// </summary>
        /// <param name="e">RecieveBeginEventArgs with information about the event</param>
        protected void OnRecieveBegin(RecieveBeginEventArgs e)
        {
            if (RecieveBegin == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = RecieveBegin.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(RecieveBegin, new object[] { this, e });
            }
            else
            {
                RecieveBegin.Invoke(this, e);
            }
        }

        /// <summary>
        /// Calls the RecieveComplete event in a thread safe manner
        /// </summary>
        /// <param name="e">RecieveCompleteEventArgs with information about the event</param>
        protected void OnRecieveComplete(RecieveCompleteEventArgs e)
        {
            if (RecieveComplete == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = RecieveComplete.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(RecieveComplete, new object[] { this, e });
            }
            else
            {
                RecieveComplete.Invoke(this, e);
            }
        }

        /// <summary>
        /// Calls the PacketRecieved event in a thread safe manner
        /// </summary>
        /// <param name="e">PacketRecievedEventArgs with information about the event</param>
        protected void OnPacketRecieved(PacketRecievedEventArgs e)
        {
            if (PacketRecieved == null)
            {
                return;
            }

            // Try a safe invoke for calls from a different thread
            ISynchronizeInvoke safeInvoker = PacketRecieved.Target as ISynchronizeInvoke;

            if (safeInvoker != null)
            {
                safeInvoker.Invoke(PacketRecieved, new object[] { this, e });
            }
            else
            {
                PacketRecieved.Invoke(this, e);
            }
        }

        #endregion

        #region Disconnection Methods

        /// <summary>
        /// Callback for finishing a disconnect request
        /// </summary>
        /// <param name="result">IAsyncResult with information about the event</param>
        private void EndDisconnect(IAsyncResult result)
        {
            // Complete the disconnect request.
            try
            {
                _socket.EndDisconnect(result);
            }
            catch (SocketException)
            {
                return;
            }

            // Finish and release resources
            FinalizeDisconnection();
        }

        /// <summary>
        /// Handles disconnect requests
        /// </summary>
        private void FinalizeDisconnection()
        {
            // Do not refire if we have already done so
            if (_firedDisconnection)
            {
                return;
            }

            // If we are still connected, we need to run a disconnect request first
            if (IsConnected)
            {
                Disconnect();
            }
            else
            {
                // Otherwise, we want to close the socket, notify, and set the flag
                lock (_syncObject)
                {
                    _firedDisconnection = true;
                    _socket.Close(20000);
                    OnDisconnected();
                }
            }
        }

        #endregion

        #region Sending Methods

        /// <summary>
        /// Callback method for finishing sending of data
        /// </summary>
        /// <param name="result">IAsyncResult with information about the event</param>
        private void EndSendPacket(IAsyncResult result)
        {
            // If we are not connected, do not continue
            if (!IsConnected)
            {
                return;
            }

            int bytesSent = 0;

            // Try to send the data
            try
            {
                bytesSent = _socket.EndSend(result);
            }
            catch (SocketException e)
            {
                // Handle specific errors
                switch (e.SocketErrorCode)
                {
                    case SocketError.ConnectionReset: // Forced Disconnect
                        FinalizeDisconnection();
                        break;
                    default: // Everything else, rethrow
                        throw;
                }
            }

            // If we have none or less than none disconnect because that indicates a problem.
            // This method should not have been called with 0 unless the connection is dead
            if (bytesSent < 0)
            {
                Disconnect();
                return;
            }

            // Notify we sent X bytes
            OnSendComplete(new SendCompleteEventArgs(bytesSent));
        }

        #endregion

        #region Recieving Methods

        /// <summary>
        /// Method to start recieving data for the header of the packet
        /// </summary>
        private void BeginReceiveHeader()
        {
            // Create buffer
            byte[] receiveBuffer = new byte[7];

            // If we are not connected, do not continue
            if (!IsConnected)
            {
                return;
            }

            // Notify we are about to recieve data of X length
            OnRecieveBegin(new RecieveBeginEventArgs(7));

            // Now make the call to get the rest of the data
            try
            {
                _socket.BeginReceive(receiveBuffer, 0, receiveBuffer.Length, SocketFlags.None, new AsyncCallback(EndReceiveHeader), receiveBuffer);
            }
            catch (SocketException e)
            {
                // Handle specific errors
                switch (e.SocketErrorCode)
                {
                    case SocketError.ConnectionReset: // Forced Disconnect
                        FinalizeDisconnection();
                        break;
                    default: // Everything else, rethrow
                        throw;
                }
            }
        }

        /// <summary>
        /// Callback method for finishing recieve for header
        /// </summary>
        /// <param name="result">IAsyncResult with information about the event</param>
        private void EndReceiveHeader(IAsyncResult result)
        {
            // Initialize local variables
            DataPacket packet = new DataPacket();
            byte[] receiveBuffer = (byte[])result.AsyncState;
            int bytesRecieved = 0;

            // If we are not connected, do not continue
            if (!IsConnected)
            {
                return;
            }

            // Try to get the header information
            try
            {
                bytesRecieved = _socket.EndReceive(result);
            }
            catch (SocketException e)
            {
                // Handle specific errors
                switch (e.SocketErrorCode)
                {
                    case SocketError.ConnectionReset: // Forced Disconnect
                        FinalizeDisconnection();
                        break;
                    default: // Everything else, rethrow
                        throw;
                }
            }

            // If we have none or less than none disconnect because that indicates a problem.
            // This method should not have been called with less than 5 unless the connection is dead
            if (bytesRecieved < 7)
            {
                Disconnect();
                return;
            }

            // Notify we have recieved data
            OnRecieveComplete(new RecieveCompleteEventArgs(bytesRecieved));

            // Set header information
            packet.Flags = (DataPacketFlags)receiveBuffer[0];
            packet.PrimaryCode = receiveBuffer[1];
            packet.SecondaryCode = receiveBuffer[2];
            packet.DataSize = BitConverter.ToInt32(receiveBuffer, 3);

            // Make sure there is content to Receive first
            if (packet.DataSize > 0)
            {
                // Now go and get the content part of the packet
                BeginReceiveContent(packet);
            }
            else
            {
                // Notify the user of the new packet information
                OnPacketRecieved(new PacketRecievedEventArgs(packet));

                // Start listening again for the header
                BeginReceiveHeader();
            }
        }

        /// <summary>
        /// Method to start recieving data for the content of the packet
        /// </summary>
        /// <param name="data">DataPacket with information about the packet</param>
        private void BeginReceiveContent(DataPacket data)
        {
            // Create buffer
            data.Data = new byte[data.DataSize];

            // If we are not connected, do not continue
            if (!IsConnected)
            {
                return;
            }

            // Notify we are about to recieve data of X length
            OnRecieveBegin(new RecieveBeginEventArgs(data.DataSize));

            // Try to get the data from the socket
            try
            {
                _socket.BeginReceive(data.Data, 0, data.DataSize, SocketFlags.None, new AsyncCallback(EndReceiveContent), new RecieveState { BytesRecieved = 0, BytesLeft = data.DataSize, Packet = data });
            }
            catch (SocketException e)
            {
                // Handle specific errors
                switch (e.SocketErrorCode)
                {
                    case SocketError.ConnectionReset: // Forced Disconnect
                        FinalizeDisconnection();
                        break;
                    default: // Everything else, rethrow
                        throw;
                }
            }
        }

        /// <summary>
        /// Callback method for finishing recieve for content
        /// </summary>
        /// <param name="result">IAsyncResult with information about the event</param>
        private void EndReceiveContent(IAsyncResult result)
        {
            // Initialize local variables
            RecieveState state = (RecieveState)result.AsyncState;
            int bytesRecieved = 0;

            // If we are not connected, do not continue
            if (!IsConnected)
            {
                return;
            }

            // Try to get the data from the socket
            try
            {
                bytesRecieved = _socket.EndReceive(result);
            }
            catch (SocketException e)
            {
                // Handle specific errors
                switch (e.SocketErrorCode)
                {
                    case SocketError.ConnectionReset: // Forced Disconnect
                        FinalizeDisconnection();
                        break;
                    default: // Everything else, rethrow
                        throw;
                }
            }

            // Modify our current status
            state.BytesRecieved += bytesRecieved;
            state.BytesLeft -= bytesRecieved;

            // This method should not have been called with less than the data length unless the size
            // is more than what is currently in place. If that is the case, then we want to recieve more
            if (state.BytesLeft > 0 && bytesRecieved > 0)
            {
                try
                {
                    _socket.BeginReceive(state.Packet.Data, state.BytesRecieved, state.BytesLeft, SocketFlags.None, new AsyncCallback(EndReceiveContent), state);
                }
                catch (SocketException)
                {
                    Disconnect();
                }

                // Leave this method to continue recieving
                return;
            }

            // If we have none or less than none disconnect because that indicates a problem.
            // This method should not have been called with less than 0 unless the connection is dead
            if (bytesRecieved <= 0)
            {
                Disconnect();
                return;
            }

            // Notify we have recieved data
            OnRecieveComplete(new RecieveCompleteEventArgs(bytesRecieved));

            // Otherwise just complete the process. 
            OnPacketRecieved(new PacketRecievedEventArgs(state.Packet));

            // Start listening again
            BeginReceiveHeader();
        }

        #endregion
    }
}